<script setup lang="ts" name="BubbleMenu">
import { i18n } from "@/locales";
import type { Editor } from "@/tiptap";
import { PluginKey } from "@/tiptap/pm";
import { computed, nextTick, onBeforeUnmount, ref, watch, type PropType } from "vue";
import LucideReplace from "~icons/lucide/replace";
import LucideReplaceAll from "~icons/lucide/replace-all";
import MdiArrowDown from "~icons/mdi/arrow-down";
import MdiArrowUp from "~icons/mdi/arrow-up";
import MdiClose from "~icons/mdi/close";
import MdiFormatLetterCase from "~icons/mdi/format-letter-case";
import MdiFormatLetterMatches from "~icons/mdi/format-letter-matches";
import MdiRegex from "~icons/mdi/regex";
import type { SearchAndReplacePluginState } from "./SearchAndReplacePlugin";

const props = defineProps({
  editor: {
    type: Object as PropType<Editor>,
    required: true,
  },
  pluginKey: {
    type: Object as PropType<PluginKey<SearchAndReplacePluginState>>,
    required: true,
  },
  visible: {
    type: Boolean,
    default: false,
  },
});

const searchTerm = ref<string>("");
const replaceTerm = ref<string>("");
const regex = ref<boolean>(false);
const caseSensitive = ref<boolean>(false);
const matchWord = ref<boolean>(false);
const flag = ref<boolean>(false);

const findState = computed(() => {
  void flag.value;
  const { editor, pluginKey } = props;
  if (!editor || !pluginKey) {
    return {
      findIndex: 0,
      findCount: 0,
    };
  }
  const state = pluginKey.getState(editor.state);
  return {
    findIndex: state?.findIndex || 0,
    findCount: state?.findCount || 0,
  };
});

const findNextSearchResult = () => {
  props.editor.commands.findNext();
  flag.value = !flag.value;
};

const findPreviousSearchResult = () => {
  props.editor.commands.findPrevious();
  flag.value = !flag.value;
};

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const updateSearchReplace = (value: any) => {
  const { editor, pluginKey } = props;
  if (!editor || !pluginKey) {
    return;
  }
  const tr = editor.state.tr;
  tr.setMeta(pluginKey, value);
  editor.view.dispatch(tr);
  flag.value = !flag.value;
};

const replace = () => {
  props.editor.commands.replace();
  flag.value = !flag.value;
};

const replaceAll = () => {
  props.editor.commands.replaceAll();
  flag.value = !flag.value;
};

const handleCloseSearch = () => {
  props.editor.commands.closeSearch();
};

watch(
  () => searchTerm.value.trim(),
  (val, oldVal) => {
    if (val !== oldVal) {
      updateSearchReplace({
        setSearchTerm: val,
      });
    }
  }
);

watch(
  () => replaceTerm.value.trim(),
  (val, oldVal) => {
    if (val !== oldVal) {
      updateSearchReplace({
        setReplaceTerm: val,
      });
    }
  }
);

watch(
  () => regex.value,
  (val, oldVal) => {
    if (val !== oldVal) {
      updateSearchReplace({
        setRegex: val,
      });
    }
  }
);

watch(
  () => caseSensitive.value,
  (val, oldVal) => {
    if (val !== oldVal) {
      updateSearchReplace({
        setCaseSensitive: val,
      });
    }
  }
);

watch(
  () => matchWord.value,
  (val, oldVal) => {
    if (val !== oldVal) {
      updateSearchReplace({
        setMatchWord: val,
      });
    }
  }
);

const searchInput = ref<HTMLInputElement | null>(null);

watch(
  () => props.visible,
  (val) => {
    if (val) {
      nextTick(() => {
        searchInput.value?.focus();
      });
    }
  }
);

// 监听编辑器更新事件（带防抖优化）
let updateTimer: ReturnType<typeof setTimeout> | null = null;
const updateHandler = () => {
  // 只在查找替换弹窗可见且有搜索词时才更新
  if (!props.visible || !searchTerm.value.trim()) {
    return;
  }
  
  // 防抖：避免频繁更新
  if (updateTimer) {
    clearTimeout(updateTimer);
  }
  
  updateTimer = setTimeout(() => {
    flag.value = !flag.value;
    updateTimer = null;
  }, 100); // 100ms 防抖延迟
};

// 注册编辑器更新监听
if (props.editor) {
  props.editor.on("update", updateHandler);
}

// 组件卸载时移除监听和清理定时器
onBeforeUnmount(() => {
  if (updateTimer) {
    clearTimeout(updateTimer);
  }
  if (props.editor) {
    props.editor.off("update", updateHandler);
  }
});
</script>
<template>
  <Transition v-show="visible" appear name="slide">
    <div
      class="search-replace-container"
      @keydown.escape.prevent="handleCloseSearch"
    >
      <section class="search-replace-section">
        <div class="search-row">
          <div class="search-input-wrapper">
            <input
              ref="searchInput"
              v-model="searchTerm"
              type="text"
              class="search-input"
              :placeholder="
                i18n.global.t(
                  'editor.extensions.search_and_replace.search_placeholder'
                )
              "
              tabindex="2"
              @keydown.enter.prevent="findNextSearchResult"
            />
            <div class="search-options">
              <button
                :title="
                  i18n.global.t(
                    'editor.extensions.search_and_replace.case_sensitive'
                  )
                "
                type="button"
                class="search-option-button"
                :class="{ active: caseSensitive }"
                @click="caseSensitive = !caseSensitive"
              >
                <MdiFormatLetterCase></MdiFormatLetterCase>
              </button>
              <button
                :title="
                  i18n.global.t(
                    'editor.extensions.search_and_replace.match_word'
                  )
                "
                type="button"
                class="search-option-button"
                :class="{ active: matchWord }"
                @click="matchWord = !matchWord"
              >
                <MdiFormatLetterMatches></MdiFormatLetterMatches>
              </button>
              <button
                :title="
                  i18n.global.t(
                    'editor.extensions.search_and_replace.use_regex'
                  )
                "
                type="button"
                class="search-option-button"
                :class="{ active: regex }"
                @click="regex = !regex"
              >
                <MdiRegex></MdiRegex>
              </button>
            </div>
          </div>
          <div class="search-result-info">
            <div v-if="findState.findCount === 0">
              <span :class="{ 'not-found': searchTerm.length > 0 }">{{
                i18n.global.t("editor.extensions.search_and_replace.not_found")
              }}</span>
            </div>
            <div v-else>
              <span>
                {{
                  i18n.global.t(
                    "editor.extensions.search_and_replace.occurrence_found",
                    {
                      index: findState.findIndex + 1,
                      total: findState.findCount,
                    }
                  )
                }}
              </span>
            </div>
          </div>
          <div class="search-actions">
            <button
              :title="
                i18n.global.t(
                  'editor.extensions.search_and_replace.find_previous'
                )
              "
              type="button"
              class="search-action-button"
              :class="{ enabled: findState.findCount > 0 }"
              :disabled="findState.findCount === 0"
              @click="findPreviousSearchResult"
            >
              <MdiArrowUp></MdiArrowUp>
            </button>
            <button
              :title="
                i18n.global.t('editor.extensions.search_and_replace.find_next')
              "
              type="button"
              class="search-action-button"
              :class="{ enabled: findState.findCount > 0 }"
              :disabled="findState.findCount === 0"
              @click="findNextSearchResult"
            >
              <MdiArrowDown></MdiArrowDown>
            </button>
            <button
              :title="
                i18n.global.t('editor.extensions.search_and_replace.close')
              "
              type="button"
              class="search-action-button close-button"
              @click="handleCloseSearch"
            >
              <MdiClose></MdiClose>
            </button>
          </div>
        </div>

        <div class="replace-row">
          <div class="replace-input-wrapper">
            <input
              v-model="replaceTerm"
              type="text"
              class="replace-input"
              :placeholder="
                i18n.global.t(
                  'editor.extensions.search_and_replace.replace_placeholder'
                )
              "
              tabindex="2"
              @keydown.enter.prevent="replace"
            />
          </div>
          <div class="replace-actions">
            <button
              :title="
                i18n.global.t('editor.extensions.search_and_replace.replace')
              "
              type="button"
              class="replace-action-button"
              :class="{ enabled: findState.findCount > 0 }"
              :disabled="findState.findCount === 0"
              @click="replace"
            >
              <LucideReplace></LucideReplace>
            </button>
            <button
              :title="
                i18n.global.t(
                  'editor.extensions.search_and_replace.replace_all'
                )
              "
              type="button"
              class="replace-action-button"
              :class="{ enabled: findState.findCount > 0 }"
              :disabled="findState.findCount === 0"
              @click="replaceAll"
            >
              <LucideReplaceAll></LucideReplaceAll>
            </button>
          </div>
        </div>
      </section>
    </div>
  </Transition>
</template>
<style lang="scss" scoped>
.search-replace-container {
  position: absolute;
  right: 1.25rem; // right-5
  top: 0;
  float: right;
  display: flex;
  min-width: 500px; // min-w-[500px]
  justify-content: flex-end;
  border-radius: 0.25rem; // rounded
  background-color: white;
  padding: 0.25rem; // p-1
  padding-top: 0.5rem !important; // !pt-2
  box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); // shadow
  pointer-events: auto;
}

.search-replace-section {
  display: flex;
  width: 100%;
  flex-direction: column;
  gap: 0.25rem; // gap-1
}

.search-row,
.replace-row {
  position: relative;
  display: flex;
  align-items: center;
}

.search-input-wrapper,
.replace-input-wrapper {
  position: relative;
  width: 100%;
  max-width: 55%; // max-w-[55%]
  box-sizing: border-box;
}

.search-input,
.replace-input {
  display: block;
  width: 100%;
  box-sizing: border-box;
  border-radius: 0.25rem; // rounded
  border: 1px solid var(--editor-input-border) !important;
  border-style: solid !important;
  background-color: var(--editor-input-bg); // bg-gray-50
  padding: 0.25rem; // p-1
  padding-left: 0.5rem; // ps-2
  font-size: 0.875rem !important; // !text-sm
  line-height: 1.75rem !important; // !leading-7
  color: var(--editor-input-text);

  &:focus {
    border-color: var(--editor-input-focus-border);
    outline: none;
    box-shadow: 0 0 0 1px var(--editor-input-focus-border);
  }
}

.search-input {
  padding-right: 5.5rem !important; // !pr-[5.5rem]
}

.search-options {
  position: absolute;
  top: 0; // inset-y-0
  bottom: 0;
  right: 0; // end-0
  display: flex;
  align-items: center;
  gap: 0.25rem; // gap-1
  padding-right: 0.25rem; // pr-1
}

.search-option-button,
.search-action-button,
.replace-action-button {
  border-radius: 0.125rem; // rounded-sm
  padding: 0.125rem; // p-0.5
  border: none;
  background-color: transparent;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;

  &:hover {
    background-color: #e5e7eb; // hover:bg-gray-200
  }

  // 图标大小
  :deep(svg) {
    width: 1.2em;
    height: 1.2em;
  }
}

.search-option-button.active {
  background-color: #bfdbfe !important; // !bg-blue-200
  outline: 1px solid #3b82f6; // outline outline-1 outline-blue-500

  &:hover {
    background-color: #bfdbfe !important; // hover:!bg-blue-200
  }
}

.search-result-info {
  margin-left: 0.5rem; // mx-2
  margin-right: 0.5rem;
  min-width: 130px; // min-w-[130px]
  font-size: 0.875rem; // text-sm

  .not-found {
    color: #dc2626; // text-red-600
  }
}

.search-actions {
  position: absolute;
  right: 0;
  display: flex;
  height: 100%;
  align-items: center;
}

.search-action-button,
.replace-action-button {
  opacity: 0.5; // opacity-50

  &.enabled {
    opacity: 1 !important; // !opacity-100

    &:hover {
      background-color: #e5e7eb !important; // hover:!bg-gray-200
    }
  }
}

.search-action-button.close-button {
  opacity: 1;
}

.replace-actions {
  margin-left: 0.5rem; // mx-2
  margin-right: 0.5rem;
  display: flex;
  align-items: center;
  gap: 0.5rem; // gap-2
}
</style>

<style>
.slide-enter-active,
.slide-leave-active {
  transition: transform 0.25s;
}

.slide-enter-from,
.slide-leave-to {
  transform: translateY(-100%);
}

.slide-enter-to,
.slide-leave-from {
  transform: translateY(0);
}
</style>
